Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 2


In the previous article Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 1, I discussed the need for Business Process Flow and related Stage transitioning in code.  I also showed a previous version of the workflow activity that used LINQ. With Microsoft CRM’s recent rebranding (now Dynamics 365), how you interact with the code base has changed and problems can occur.  With that I described a recent problem centered around transitioning stages that were out of sequence.  These stages also may or may not have had any field requirements assigned to them.  This issue seems to have surfaced from a recent version update that has since closed the window on a long outstanding bug. You are now only allowed to transition phases in order and you must complete all the field requirements before transitioning further. If you do not follow these rules then you’ll get the dreaded Invalid Stage Transition error.

Business Process Flow (BPF) and Stage Transitioning

Microsoft’s Developer Network Documentation: Model Business Process Flows

Keep in mind that the overall code can be condensed as I’ve inserted excessive tracing in order to convey how each section of the code works. I also use a tool called XrmToolBox along with the plugin DLaB Early Bound Generator for creating the necessary class objects that I interact with. Large sections of the code were taken from the provided Microsoft Documentation so kudos to the Microsoft team that created it. However, there are a few code lines here and there that I found rather obscure. I’ll try to dive into those sections in greater detail along with explaining the need for what seems to be randomly placed System.Threading.Thread.Sleep methods; for my Dynamics 365 Online instance, they seem to be valid. However, take them with a grain of salt when applying them to both your development and production instances as your systems speed and efficiency may differ greatly with mine. Development is usually slower and may need a longer pause than production.

Note: Please keep in mind that when you see the abbreviation “BPF”, I’m really just saying “Business Process Flow”.

Now, lets get to the solution.  This bit of code below is used twice in the solution, this first instance was used to pull a collection of BPFs associated with the parent entity. Depending upon how many times the parent entity has switched BPFs will determine the processCount in the returned response.

          var processInstancesRequest=new RetrieveProcessInstancesRequest {
            EntityId=theEntity.Id
            ,EntityLogicalName=theEntity.LogicalName
          };

          var processInstancesResponse=(RetrieveProcessInstancesResponse)serviceProxy.Execute(processInstancesRequest);
          var processCount=processInstancesResponse.Processes.Entities.Count;

Assuming you have at least one BPF already associated to the parent entity (and you should so no worries), we can move beyond the count check. The next bit of code simply provides tracing details and retrieves our first input variable BpfEntityReference. From that we can pull the Unique Name, which in turn we’ll use to build our QueryExpression that’ll execute against the Workflow entity. The query is used to retrieve the required Id field in the Workflow entity of the BPF we plan on switching too.

if (processCount>0) {
  //*** Verify Current Process Instance ------------------------------------------------------------------------------------------------------
  var initialProcessInstance = processInstancesResponse.Processes.Entities[0]; //*** First Entity record is the Active Process Instance
  var initialProcessInstanceId = initialProcessInstance.Id; //*** Active Process Instance Id to be used later for retrieval of the active path of the process instance
  var initialProcessInstanceName = initialProcessInstance.Attributes[CrmEarlyBound.Workflow.Fields.Name];

  tracingService.Trace("{0}: [1]Current BPF Instance Name: {1}, BPF Instance Id: {2}",CHILD_CLASS_NAME,initialProcessInstanceName,initialProcessInstanceId.ToString());
  tracingService.Trace("{0}: [1]Associated Process Instance Count: {1}",CHILD_CLASS_NAME,processCount);

  var message = string.Empty;
  for (var i=0; i<processCount; i++) {
    message=message+" "+processInstancesResponse.Processes.Entities[i].Attributes[CrmEarlyBound.Workflow.Fields.Name]+",";
  }

  tracingService.Trace("{0}: [1]Associated Process Instances:{1}",CHILD_CLASS_NAME,message.TrimEnd(message[message.Length-1]));

  //*** Grab the Workflow Entity -----------------------------------------------------------------------------------------------------------------------
  var bpfEntityRef = this.BpfEntityReference.Get<EntityReference>(executionContext);
  var colSet = new ColumnSet();

  colSet.AddColumn(CrmEarlyBound.Workflow.Fields.UniqueName);

  var bpfEntity = serviceProxy.Retrieve(bpfEntityRef.LogicalName,bpfEntityRef.Id,colSet);

  tracingService.Trace("{0}: Searching for BPF Unique Name: {1}, BPF Process Id: {2}",CHILD_CLASS_NAME,bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(),bpfEntity.Id.ToString());

  var bpfStageName = this.BpfStageName.Get<string>(executionContext).Trim(); //*** Current Stage
  var qe = new QueryExpression {
  EntityName=CrmEarlyBound.Workflow.EntityLogicalName
  ,ColumnSet=new ColumnSet(new string[] {
        CrmEarlyBound.Workflow.Fields.Name
  })
  ,Criteria=new FilterExpression {
  Conditions={
          new ConditionExpression {
            AttributeName=CrmEarlyBound.Workflow.Fields.UniqueName
            ,Operator=ConditionOperator.Equal
            ,Values={
              bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName]
            }
          }
  }
  }
  ,NoLock=true
  ,Distinct=false
  };

  tracingService.Trace("{0}: Built BPF Query, Now Executing...",CHILD_CLASS_NAME);

  var bpfByQuery = serviceProxy.RetrieveMultiple(qe).Entities.FirstOrDefault(); //*** Execute Query w/ Filter Expressions - ONLY 1 IS RETURNED

Executing the query should yield at least one result from the Workflow entity. Again, this is the BPF we plan on switching too. We really only need the bpfId field for switching but I also grab the name for tracing purposes.  I really haven’t tested in depth the 2 second pause that has been inserted into the code. It was advised in the Microsoft Documentation but without a reason as to why, so I simply went with it for now.  I’ll update this post if I can determine the need for it or if I permanently remove it.

            //*** Switch BPF Instance [DEPRECATED in D365 v9.1] --------------------------------------------------------------------------------------------------
            if (bpfByQuery!=null&&bpfByQuery.Id!=Guid.Empty) {
              tracingService.Trace("{0}: Found Matching BPF...",CHILD_CLASS_NAME);

              var bpfId=bpfByQuery.Id;
              var bpfEntityName=bpfByQuery.GetAttributeValue<string>(CrmEarlyBound.Workflow.Fields.Name);

              tracingService.Trace("{0}: Switching to BPF Instance Name: {1}, BPF Instance Id: {2}",CHILD_CLASS_NAME,bpfEntityName,bpfId.ToString());
              //System.Threading.Thread.Sleep(2000); // Wait for 2 seconds before switching the process
              var setProcReq=new SetProcessRequest { // Set New/Same BPF *** DEPRECATED v9.1 ***
                Target=new EntityReference(theEntity.LogicalName,theEntity.Id)
                ,NewProcess=new EntityReference(CrmEarlyBound.Workflow.EntityLogicalName,bpfId)
              };

              tracingService.Trace("{0}: ***Ready To Update - BPF",CHILD_CLASS_NAME);
              var setProcResp=(SetProcessResponse)serviceProxy.Execute(setProcReq);
              tracingService.Trace("{0}: ***Updated",CHILD_CLASS_NAME);
            } else {
              tracingService.Trace("{0}: Found NO Matching BPF Unique Name: {1}",CHILD_CLASS_NAME,bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName]);
            }

The code here is a repeat and helpful in determining if your initial BPF change was successful. From there you can print out each of the BPFs that are associated to the parent entity. The first BPF in the list will always be the active BPF.

//*** Verify if the Process Instance was switched successfully for the Entity record
            processInstancesRequest=new RetrieveProcessInstancesRequest {
              EntityId=theEntity.Id
              ,EntityLogicalName=theEntity.LogicalName
            };

            processInstancesResponse=(RetrieveProcessInstancesResponse)serviceProxy.Execute(processInstancesRequest);
            processCount=processInstancesResponse.Processes.Entities.Count;

            if (processCount>0) {
              //*** Verify Successful Process Instance Switch ------------------------------------------------------------------------------------------------------
              var activeProcessInstance=processInstancesResponse.Processes.Entities[0]; //*** First Entity record is the Active Process Instance
              var activeProcessInstanceId=activeProcessInstance.Id; //*** Active Process Instance Id to be used later for retrieval of the active path of the process instance
              var activeProcessInstanceName=activeProcessInstance.Attributes[CrmEarlyBound.Workflow.Fields.Name];

              tracingService.Trace("{0}: [2]Current BPF Instance Name: {1}, BPF Instance Id: {2}",CHILD_CLASS_NAME,activeProcessInstanceName,activeProcessInstanceId.ToString());
              tracingService.Trace("{0}: [2]Associated Process Instance Count: {1}",CHILD_CLASS_NAME,processCount);

              message=string.Empty;
              for (var i=0; i<processCount; i++) {
                message=message+" "+processInstancesResponse.Processes.Entities[i].Attributes[CrmEarlyBound.Workflow.Fields.Name]+",";
              }

              tracingService.Trace("{0}: [2]Associated Process Instances:{1}",CHILD_CLASS_NAME,message.TrimEnd(message[message.Length-1]));

This section of code loops through each Stage of the active BPF and displays each of the Stages information along with determining the starting and ending positions of the required transition. Microsoft states that you should only transition one Stage at a time but where’s the fun in that. As long as you don’t have any field requirements in the stages that you are either switching from or skipping over then you can transition as many stages as you like.

//*** Retrieve the Active Stage ID of the Active Process Instance
var activeStageId = new Guid(activeProcessInstance.Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());
var activeStagePosition = 0;
var newStageId = new Guid();
var newStagePosition = 0;

//*** Retrieve the BPF Stages in the active path of the Active Process Instance
var activePathRequest = new RetrieveActivePathRequest {
  ProcessInstanceId = activeProcessInstanceId
};
var activePathResponse = (RetrieveActivePathResponse)serviceProxy.Execute(activePathRequest);

tracingService.Trace("{0}: Retrieved the BPF Stages in the Active Path of the Process Instance:", CHILD_CLASS_NAME);

for (var i = 0; i < activePathResponse.ProcessStages.Entities.Count; i++) {
  var curStageName = activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.StageName].ToString();

  tracingService.Trace("{0}: Looping Through Stage #{1}: {2} (StageId: {3}, IndexId: {4})", CHILD_CLASS_NAME, i + 1, curStageName, activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId], i);
  //*** Retrieve the Active Stage Name and Stage Position based on a successful match of the activeStageId
  if (activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].Equals(activeStageId)) {
    activeStagePosition = i;
    tracingService.Trace("{0}: Concerning the Process Instance -- Initial Active Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, curStageName, activeStageId);
  }
  //*** Retrieve the New Stage Id, Stage Name, and Stage Position based on a successful match of the stagename
  if (curStageName.Equals(bpfStageName, StringComparison.InvariantCultureIgnoreCase)) {
    newStageId = new Guid(activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());
    newStagePosition = i;
    tracingService.Trace("{0}: Concerning the Process Instance -- Desired New Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, curStageName, newStageId);
  }
}

Partial Log Example:

This snapshot view of the tracing out put displays an example all the stages related to one of my BPFs to give you an idea of what’s happening in the above code.

SetBizProcessFlowV2: Looping Through Stage #1: INITIATION (StageId: f0be05ac-51be-498c-b1d5-ebfc036095e2, IndexId: 0)
SetBizProcessFlowV2: Looping Through Stage #2: PREPARATION (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e, IndexId: 1)
SetBizProcessFlowV2: Concerning the Process Instance -- Initial Active Stage Name: PREPARATION (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e)
SetBizProcessFlowV2: Looping Through Stage #3: REVIEW (StageId: 0c63beac-b1dc-4db5-a4b5-0ffd9ac96cc9, IndexId: 2)
SetBizProcessFlowV2: Looping Through Stage #4: ATP (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162, IndexId: 3)
SetBizProcessFlowV2: Concerning the Process Instance -- Desired New Stage Name: ATP (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162)
SetBizProcessFlowV2: Looping Through Stage #5: PAPER PROCESSING (StageId: 350d89b3-df17-48d2-a930-91012686b62f, IndexId: 4)
SetBizProcessFlowV2: Looping Through Stage #6: DELIVERY (StageId: 4c3ed26b-4f86-450b-83c2-1c32d65cc6af, IndexId: 5)
SetBizProcessFlowV2: Looping Through Stage #7: FINALIZATION (StageId: d0bbe23f-e72f-48de-9b0e-ce410469a36f, IndexId: 6)
SetBizProcessFlowV2: Looping Through Stage #8: COMPLETED (StageId: d5242580-a6c3-4835-99a1-7b4a6cd910dc, IndexId: 7)

The previous section determined the transition requirements, while this section performs the actual transition via a loop so as to not illegally transition out of sequence. An illegal transition will result in an Invalid Stage Transition error. This part was rather frustrating for me because Microsoft’s Documentation was severely lacking in listing out what all affects the BPF transition process.

If your BPF has no field requirements (fields that must contain data before you change a Stage), you can still get the nasty Invalid Stage Transition error message while transitioning because you may have fields on the entity that are set or have been recently set to required. To help illuminate this issue I’ve placed a Try-Catch statement below that will catch the error and print out the child attribute clientdata of which contains all the fields of the entity.  This also includes the Boolean value used to identify if the field is required or not. This should help you in determining where the breakdown is actually occurring.

//***Update the Business Process Flow Instance record to the desired Active Stage
Entity retrievedProcessInstance;
ColumnSet columnSet;
var stageShift = newStagePosition - activeStagePosition;

if (stageShift > 0) {
  tracingService.Trace("{0}: Number of Stages Shifting Forward: {1}", CHILD_CLASS_NAME, stageShift);
  //*** Stages only move in 1 direction --> Forward
  for (var i = activeStagePosition; i <= newStagePosition; i++) {
    System.Threading.Thread.Sleep(1000);
    //*** Retrieve the Stage Id of the next stage that you want to set as active
    var newStageName = activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.StageName].ToString();
    newStageId = new Guid(activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());

    tracingService.Trace("{0}: Setting To Stage #{1}: {2} (StageId: {3}, IndexId: {4})", CHILD_CLASS_NAME, i + 1, newStageName, newStageId, i);
    //*** Retrieve the BpfEntityName Instance record to update its Active Stage
    columnSet = new ColumnSet();
    columnSet.AddColumn(ACTIVE_STAGE_ID);
    retrievedProcessInstance = serviceProxy.Retrieve(bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(), activeProcessInstanceId, columnSet);
    //*** Set the next Stage as the Active Stage
    retrievedProcessInstance[ACTIVE_STAGE_ID] = new EntityReference(CrmEarlyBound.ProcessStage.EntityLogicalName, newStageId); //(ProcessStage.EntityLogicalName, activeStageId);

    try {
      tracingService.Trace("{0}: ***Ready To Update -- BPF Stage", CHILD_CLASS_NAME);
      serviceProxy.Update(retrievedProcessInstance);
      tracingService.Trace("{0}: ***Updated", CHILD_CLASS_NAME);
    } catch (FaultException<OrganizationServiceFault> ex) { //*** Determine BPF Stage Requirements
      foreach (var stageAttribute in activePathResponse.ProcessStages.Entities[i].Attributes) {
        if (stageAttribute.Key.Equals("clientdata")) {
          tracingService.Trace("{0}: Attribute Key: {1}, Value: {2}", CHILD_CLASS_NAME, stageAttribute.Key, stageAttribute.Value.ToString());
          break;
        }
      }

      tracingService.Trace(FullStackTraceException.Create(ex).ToString());
      throw;
    }
  }
} else {
  tracingService.Trace("{0}: Number of Stages Shifting Backwards: {1}", CHILD_CLASS_NAME, stageShift);
}

Finally, this last bit of code is purely optional and really just meant for debugging purposes. The tracing output provides information on whether the active stage actually transitioned to the desired stage specified in the workflow activity parameters.

//***Retrieve the Business Process Flow Instance record again to verify its Active Stage information
columnSet = new ColumnSet();
columnSet.AddColumn(ACTIVE_STAGE_ID);
retrievedProcessInstance = serviceProxy.Retrieve(bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(), activeProcessInstanceId, columnSet);

var activeStageEntityRef = retrievedProcessInstance[ACTIVE_STAGE_ID] as EntityReference;
if (activeStageEntityRef != null) {
  if (activeStageEntityRef.Id.Equals(newStageId)) {
    tracingService.Trace("{0}: Concerning the Process Instance -- Modified -- Active Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, activeStageEntityRef.Name, activeStageEntityRef.Id);
  }
}

I hope that this walk-thru has adequately demonstrated in greater detail how to properly perform a Business Process Flow change and/or BPF Stage transition in code.  While a lot of the code reflects what was written in Microsoft’s Documentation concerning the BPF, I think the additional tracing and explanations should at least point in in the right direction when trying to decipher potential shortcomings that may occur when interacting with the BPF.  In my next post for this series I’ll show you how to deploy your new custom workflow activity into your production solution.

I’ll post the project code up on GitHub soon so please check back…

Related Articles

 

Leave a comment